/*
 * Mobicents, Communications Middleware
 * 
 * Copyright (c) 2008, Red Hat Middleware LLC or third-party
 * contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Middleware LLC.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 *
 * Boston, MA  02110-1301  USA
 */
package org.mobicents.diameter.server.bootstrap;

import java.io.File;
import java.io.FileFilter;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.jboss.kernel.Kernel;
import org.jboss.kernel.plugins.deployment.xml.BasicXMLDeployer;


/**
 * Simplified deployement framework designed for hot deployement of endpoints and media components.
 * 
 * Deployement is represented by tree of folders. Each folder may contains one or more deployement descriptors. The most
 * top deployment directory is referenced as root. Maindeployer creates recursively HDScanner for root and each nested
 * directoty. The HDScanner corresponding to the root directory is triggered periodicaly by local timer and in it order
 * starts nested scanners recursively.
 * 
 * @author kulikov
 * @author amit bhayani
 */
public class MainDeployer implements ContainerOperations{

	/** JBoss microconatiner kernel */
	private Kernel kernel;
	/** Basic deployer */
	private BasicXMLDeployer kernelDeployer;
	/** Interval for scanning root deployment directory */
	private int scanPeriod;
	/** Filter for selecting descriptors */
	private FileFilter fileFilter;
	/** Root deployment directory as string */
	private Set<String> path;
	/** Root deployment directory as file object */
	private File[] root;
	/** Scanner trigger */
	private ScheduledExecutorService executor = null;
	/** Trigger's controller */
	private ScheduledFuture activeScan;


	/** Logger instance */
	private Logger logger = Logger.getLogger(MainDeployer.class);

	/**
	 * Creates new instance of deployer.
	 */
	public MainDeployer() {
		executor = Executors.newSingleThreadScheduledExecutor(new ScannerThreadFactory());
	}

	/**
	 * Gets the current value of the period used for scanning deployement directory.
	 * 
	 * @return the value of the period in milliseconds.
	 */
	public int getScanPeriod() {
		return scanPeriod;
	}

	/**
	 * Modifies value of the period used to scan deployement directory.
	 * 
	 * @param scanPeriod
	 *            the value of the period in milliseconds.
	 */
	public void setScanPeriod(int scanPeriod) {
		this.scanPeriod = scanPeriod;
	}

	/**
	 * Gets the path to the to the root deployment directory.
	 * 
	 * @return path to deployment directory.
	 */
	public Set<String> getPath() {
		return path;
	}

	/**
	 * Modify the path to the root deployment directory
	 * 
	 * @param path
	 */
	public void setPath(Set<String> path) {
		this.path = path;
		root = new File[path.size()];
		int count = 0;
		for (String s : path) {
			root[count++] = new File(s);
		}
	}

	/**
	 * Gets the filter used by Deployer to select files for deployement.
	 * 
	 * @return the file filter object.
	 */
	public FileFilter getFileFilter() {
		return fileFilter;
	}

	/**
	 * Assigns file filter used for selection files for deploy.
	 * 
	 * @param fileFilter
	 *            the file filter object.
	 */
	public void setFileFilter(FileFilter fileFilter) {
		this.fileFilter = fileFilter;
	}

	/**
	 * Starts main deployer.
	 * 
	 * @param kernel
	 *            the jboss microntainer kernel instance.
	 * @param kernelDeployer
	 *            the jboss basic deployer.
	 */
	public void start(Kernel kernel, BasicXMLDeployer kernelDeployer) {

		this.kernel = kernel;
		this.kernelDeployer = kernelDeployer;

		// If scanPeriod is set to -ve, reset to 0
		if (scanPeriod < 0) {
			this.scanPeriod = 0;
		}

		// If scanPeriod is set to less than 1 sec but greater than 0 millisec, re-set to 1 sec
		if (scanPeriod < 1000 && scanPeriod > 0) {
			this.scanPeriod = 1000;
		}

		for (File f : root) {
			// create deployement scanner and do first deployement
			HDScanner scanner = new HDScanner(f);
			scanner.run();

			// hot-deployment disabled for scan period set to 0
			if (!(scanPeriod == 0)) {
				activeScan = executor.scheduleAtFixedRate(scanner, scanPeriod, scanPeriod, TimeUnit.MILLISECONDS);
			} else {
				if (logger.isDebugEnabled()) {
					logger.debug("scanPeriod is set to 0. Hot deployment disabled");
				}
			}
		}
		logger.info("[[[[[[[[[  Started " + "]]]]]]]]]");
	}

	/**
	 * Shuts down deployer.
	 */
	public void stop() {
		if (activeScan != null) {
			activeScan.cancel(true);
		}
		logger.info("Stopped");
	}

	/**
	 * Deploys components using specified deployement descriptor.
	 * 
	 * @param file
	 *            the reference to the deployment descriptor
	 * @throws java.lang.Throwable
	 */
	private void deploy(File file) {
		logger.info("Deploying " + file);

		String filePath = file.getPath();
		if (filePath.endsWith(".xml")) {
			try {
				kernelDeployer.deploy(file.toURI().toURL());
				kernelDeployer.validate();
				logger.info("Deployed " + file);
			} catch (Throwable t) {
				logger.info("Could not deploy " + file, t);
			}
		}
	}

	/**
	 * Undeploys components specified in deployment descriptor.
	 * 
	 * @param file
	 *            the reference to the deployment descriptor.
	 */
	private void undeploy(File file) {
		logger.info("Undeploying " + file);
		String filePath = file.getPath();
		if (filePath.endsWith(".xml")) {
			try {
				kernelDeployer.undeploy(file.toURI().toURL());
			} catch (MalformedURLException e) {
			}
			logger.info("Undeployed " + file);
		}
	}

	/**
	 * Redeploys components specified in deployment descriptor.
	 * 
	 * This method subsequently performs undeployment and deployment procedures.
	 * 
	 * @param file
	 *            the reference to the deployment descriptor.
	 */
	private void redeploy(File file) {
		undeploy(file);
		deploy(file);
	}

	/**
	 * Deployment scanner.
	 * 
	 * The scanner relates to a directory in the deployement structure. It is responsible for processing all descriptors
	 * in this directotry and for triggering nested scanners.
	 */
	private class HDScanner implements Runnable {

		/** directory to which scanner relates */
		private File dir;
		/** nested scanners */
		private HashMap<File, HDScanner> scanners = new HashMap();
		/** map between descriptor and last deployed time */
		private HashMap<File, Deployment> deployments = new HashMap();

		/**
		 * Creates new instance of the scanner.
		 * 
		 * @param dir
		 *            the directory releated to this scanner.
		 */
		public HDScanner(File dir) {
			// we do not any check for file (is it directory) because we know that
			// always directory
			this.dir = dir;
		}

		/**
		 * This methods is called by local timer.
		 * 
		 * It shoudl find changes in the deployement structure and execute corresponding method:
		 * 
		 * -deploy for new descriptors; -undeploy for removed descriptors; -redeploy for updated descriptors; -create
		 * nested scanner for new nested directories; -remove nested scanner for deleted nested directories; -run nested
		 * scanners recursively.
		 */
		public void run() {
			// get the fresh list of nested files
			File[] files = dir.listFiles();

			// select list of new files and process this list
			Collection<File> list = getNew(files);
			if (!list.isEmpty()) {
				for (File file : list) {
					// again, for directories we are creating nested scanners
					// and deploying desciptors
					if (file.isDirectory()) {
						HDScanner childScanner = new HDScanner(file);
						scanners.put(file, childScanner);
						// keep reference to nested directory because we need to track
						// the case when directory will be deleted
					} else if (file.isFile() && fileFilter.accept(file)) {
						deploy(file);
					}
					deployments.put(file, new Deployment(file));
				}
			}

			// now same for removed.
			// determine list of removed files
			list = getRemoved(files);
			for (File file : list) {
				Deployment d = deployments.remove(file);
				if (d == null) {
					continue;
				}
				if (d.isDirectory()) {
					HDScanner scanner = scanners.remove(file);
					if (scanner != null) {
						scanner.undeployAll();
					}
				} else {
					undeploy(file);
				}
			}

			// redeploying
			list = getUpdates(files);
			for (File file : list) {
				// ignore directories
				if (file.isFile() && fileFilter.accept(file)) {
					redeploy(file);
				}
			}

			// Processing nested deployments
			Collection<HDScanner> nested = scanners.values();
			for (HDScanner scanner : nested) {
				scanner.run();
			}
		}

		private void undeployAll() {
			// undeploy descriptors
			Set<File> list = deployments.keySet();
			for (File file : list) {
				if (!deployments.get(file).isDirectory()) {
					undeploy(file);
				}
			}
			deployments.clear();

			// recursively undeploy nested directories
			Collection<HDScanner> nested = scanners.values();
			for (HDScanner scanner : nested) {
				scanner.undeployAll();
			}
		}

		/**
		 * Collects added descriptors from the specified list.
		 * 
		 * @param files
		 *            the list of descriptors.
		 * @return the list of new new descriptors.
		 */
		private Collection<File> getNew(File[] files) {
			ArrayList<File> list = new ArrayList();
			for (File f : files) {
				if (!deployments.containsKey(f)) {
					list.add(f);
				}
			}

			return list;
		}

		/**
		 * Collects descriptors that were deleted.
		 * 
		 * @param files
		 *            the list of currently available descriptors
		 * @return the list of deleted descriptors.
		 */
		private Collection<File> getRemoved(File[] files) {
			List<File> removed = new ArrayList();
			Set<File> list = deployments.keySet();

			for (File descriptor : list) {
				boolean found = false;
				for (File file : files) {
					if (descriptor.equals(file)) {
						found = true;
						break;
					}
				}

				if (!found) {
					removed.add(descriptor);
				}
			}

			return removed;
		}

		/**
		 * Collects descriptors that were updates.
		 * 
		 * @param files
		 *            the fresh list of descriptors.
		 * @return the list of updated descriptors.
		 */
		private Collection<File> getUpdates(File[] files) {
			ArrayList<File> list = new ArrayList();
			for (File f : files) {
				if (deployments.containsKey(f)) {
					long lastModified = deployments.get(f).lastModified();
					if (lastModified < f.lastModified()) {
						deployments.get(f).update(f.lastModified());
						list.add(f);
					}
				}
			}
			return list;
		}
	}

	/**
	 * Scanner thread factory.
	 */
	private class ScannerThreadFactory implements ThreadFactory {

		/**
		 * Creates new thread.
		 * 
		 * @param r
		 *            main deployer.
		 * @return thread object.
		 */
		public Thread newThread(Runnable r) {
			return new Thread(r, "MainDeployer");
		}
	}
	
	
	//hook jmx method to terminate container
	public void terminateContainer()
	{
		//this will trigger shoot down hook, which nicely kills container.
		System.exit(0);
	}
}
